luci-app-travelmate: sync with travelmate 2.3.0-1 master
authorDirk Brenken <[email protected]>
Thu, 11 Dec 2025 21:48:08 +0000 (22:48 +0100)
committerDirk Brenken <[email protected]>
Thu, 11 Dec 2025 21:48:33 +0000 (22:48 +0100)
Signed-off-by: Dirk Brenken <[email protected]>
applications/luci-app-travelmate/Makefile
applications/luci-app-travelmate/htdocs/luci-static/resources/view/travelmate/logtemplate.js
applications/luci-app-travelmate/htdocs/luci-static/resources/view/travelmate/overview.js
applications/luci-app-travelmate/root/usr/share/luci/menu.d/luci-app-travelmate.json
applications/luci-app-travelmate/root/usr/share/rpcd/acl.d/luci-app-travelmate.json

index 19cc8f5e301894f33a3e08cc3e8485d8cddfd68e..4ad673344fbda02dcbee56825f9aacf6b6a4d52a 100644 (file)
@@ -6,6 +6,8 @@ include $(TOPDIR)/rules.mk
 LUCI_TITLE:=LuCI support for Travelmate
 LUCI_DEPENDS:=+luci-base +luci-lib-uqr +travelmate
 
+PKG_VERSION:=2.3.0
+PKG_RELEASE:=1
 PKG_LICENSE:=Apache-2.0
 PKG_MAINTAINER:=Dirk Brenken <[email protected]>
 
index 23a8fbebe77245c4cdf6b42b905ae6f594793b91..10c552721f3fee1f2134633f4305b07070cd33b6 100644 (file)
@@ -1,33 +1,54 @@
 'use strict';
-'require fs';
+'require rpc';
+
+const callLogRead = rpc.declare({
+       object: 'log',
+       method: 'read',
+       params: ['lines', 'stream', 'oneshot'],
+       expect: {}
+});
 
 function Logview(logtag, name) {
        return L.view.extend({
-               load: function() {
-                       return Promise.all([
-                               L.resolveDefault(fs.stat('/sbin/logread'), null),
-                               L.resolveDefault(fs.stat('/usr/sbin/logread'), null)
-                       ]);
-               },
+               load: () => Promise.resolve(),
 
-               render: function(stat) {
-                       let logger = stat[0]?.path || stat[1]?.path || null;
+               render: () => {
+                       L.Poll.add(() => {
+                               return callLogRead(1000, false, true).then(res => {
+                                       const logEl = document.getElementById('logfile');
+                                       if (!logEl) return;
 
-                       if (!logger) {
-                               return E('div', { class: 'error' }, _('logread not found on system.'));
-                       }
-                       L.Poll.add(function() {
-                               return L.resolveDefault(fs.exec_direct(logger, ['-e', logtag])).then(function(res) {
-                                       var log = document.getElementById('logfile');
-                                       if (log) {
-                                               log.value = res ? res.trim() : _('No %s related logs yet!').format(name);
-                                               log.scrollTop = log.scrollHeight;
+                                       const entries = res?.log ?? [];
+                                       if (entries.length > 0) {
+                                               const filtered = entries
+                                                       .filter(entry => !logtag || entry.msg.includes(logtag))
+                                                       .map(entry => {
+                                                               const d = new Date(entry.time);
+                                                               const date = d.toLocaleDateString([], {
+                                                                       year: 'numeric',
+                                                                       month: '2-digit',
+                                                                       day: '2-digit'
+                                                               });
+                                                               const time = d.toLocaleTimeString([], {
+                                                                       hour: '2-digit',
+                                                                       minute: '2-digit',
+                                                                       second: '2-digit',
+                                                                       hour12: false
+                                                               });
+                                                               return `[${date}-${time}] ${entry.msg}`;
+                                                       });
+                                               logEl.value = filtered.join('\n');
+                                       } else {
+                                               logEl.value = _('No %s related logs yet!').format(name);
                                        }
+                                       logEl.scrollTop = logEl.scrollHeight;
                                });
                        });
+
                        return E('div', { class: 'cbi-map' }, [
                                E('div', { class: 'cbi-section' }, [
-                                       E('div', { class: 'cbi-section-descr' }, _('The syslog output, pre-filtered for messages related to: %s').format(name)),
+                                       E('div', { class: 'cbi-section-descr' },
+                                               _('The syslog output, pre-filtered for messages related to: %s').format(name)),
                                        E('textarea', {
                                                id: 'logfile',
                                                style: 'min-height: 500px; max-height: 90vh; width: 100%; padding: 5px; font-family: monospace; resize: vertical;',
@@ -37,12 +58,11 @@ function Logview(logtag, name) {
                                ])
                        ]);
                },
+
                handleSaveApply: null,
                handleSave: null,
                handleReset: null
        });
 }
 
-return L.Class.extend({
-       Logview: Logview
-});
+return L.Class.extend({ Logview });
index 15df17ebf4afd1188019d76b3340d7577ca7bc73..84c17852b36ccf4ba0588a10634a33b8f6e2f747 100644 (file)
@@ -1,4 +1,5 @@
 'use strict';
+'require dom';
 'require view';
 'require poll';
 'require fs';
 */
 function handleAction(ev) {
        let ifaceValue;
-       if (ev === 'restart') {
+       if (ev === 'restartInterface') {
                ifaceValue = String(uci.get('travelmate', 'global', 'trm_iface') || 'trm_wwan');
                return fs.exec('/etc/init.d/travelmate', ['stop'])
                        .then(fs.exec('/sbin/ifup', [ifaceValue]))
                        .then(fs.exec('/etc/init.d/travelmate', ['start']))
        }
+       if (ev === 'restartTravelmate') {
+               const map = document.querySelector('.cbi-map');
+               return dom.callClassMethod(map, 'save')
+                       .then(L.bind(ui.changes.apply, ui.changes))
+                       .then(function () {
+                               return fs.exec_direct('/etc/init.d/travelmate', ['restart']);
+                       })
+       }
        if (ev === 'setup') {
                ifaceValue = String(uci.get('travelmate', 'global', 'trm_iface') || '');
                L.ui.showModal(_('Interface Wizard'), [
                        E('p', _('To use Travelmate, you have to set up an uplink interface once. This wizard creates an IPv4- and an IPv6 alias network interface with all required network- and firewall settings.')),
                        E('div', { 'class': 'left', 'style': 'display:flex; flex-direction:column' }, [
-                               E('label', { 'class': 'cbi-input-text', 'style': 'padding-top:.5em' }, [
-                                       E('input', { 'class': 'cbi-input-text', 'id': 'iface', 'placeholder': 'trm_wwan', 'value': ifaceValue, 'maxlength': '15', 'spellcheck': 'false' }),
-                                       '\xa0\xa0\xa0',
+                               E('label', { 'class': 'cbi-input-text', 'style': 'padding-top:.5em;' }, [
+                                       E('input', { 'class': 'cbi-input-text', 'id': 'iface', 'placeholder': 'trm_wwan', 'value': ifaceValue, 'maxlength': '15', 'spellcheck': 'false', style: 'margin-right:.5em;' }),
                                        _('The uplink interface name')
                                ]),
-                               E('label', { 'class': 'cbi-input-text', 'style': 'padding-top:.5em' }, [
-                                       E('input', { 'class': 'cbi-input-text', 'id': 'zone', 'placeholder': 'wan', 'maxlength': '15', 'spellcheck': 'false' }),
-                                       '\xa0\xa0\xa0',
+                               E('label', { 'class': 'cbi-input-text', 'style': 'padding-top:.5em;' }, [
+                                       E('input', { 'class': 'cbi-input-text', 'id': 'zone', 'placeholder': 'wan', 'maxlength': '15', 'spellcheck': 'false', style: 'margin-right:.5em;' }),
                                        _('The firewall zone name')
                                ]),
-                               E('label', { 'class': 'cbi-input-text', 'style': 'padding-top:.5em' }, [
-                                       E('input', { 'class': 'cbi-input-text', 'id': 'metric', 'placeholder': '100', 'maxlength': '3', 'spellcheck': 'false' }),
-                                       '\xa0\xa0\xa0',
+                               E('label', { 'class': 'cbi-input-text', 'style': 'padding-top:.5em;' }, [
+                                       E('input', { 'class': 'cbi-input-text', 'id': 'metric', 'placeholder': '100', 'maxlength': '3', 'spellcheck': 'false', style: 'margin-right:.5em;' }),
                                        _('The interface metric')
                                ])
                        ]),
                        E('div', { 'class': 'right' }, [
                                E('button', {
                                        'class': 'cbi-button',
+                                       'style': 'float:none;margin-right:.4em;',
                                        'click': L.hideModal
                                }, _('Dismiss')),
-                               ' ',
                                E('button', {
                                        'class': 'cbi-button cbi-button-positive important',
+                                       'style': 'float:none',
                                        'click': ui.createHandlerFn(this, function (ev) {
-                                               let iface = document.getElementById('iface').value || 'trm_wwan',
-                                                       zone = document.getElementById('zone').value || 'wan',
-                                                       metric = document.getElementById('metric').value || '100';
-                                               fs.exec('/etc/init.d/travelmate', ['setup', iface, zone, metric]).then(function (rc) {
-                                                       L.hideModal();
-                                                       if (rc.code == 1) {
-                                                               ui.addNotification(null, E('p', _('Setup failed, the interface already exists!')), 'error');
-                                                       } else {
-                                                               location.reload();
-                                                       }
-                                               });
+                                               const iface = (document.getElementById('iface').value || 'trm_wwan').toLowerCase();
+                                               const zone = (document.getElementById('zone').value || 'wan').toLowerCase();
+                                               const metric = document.getElementById('metric').value.replace(/\D/g, '') || '100';
+                                               fs.exec('/etc/init.d/travelmate', ['setup', iface, zone, metric])
+                                                       .then(function (rc) {
+                                                               L.hideModal();
+                                                               switch (rc.code) {
+                                                                       case 1:
+                                                                               ui.addNotification(null, E('p', _('The interface already exists!')), 'info');
+                                                                               break;
+                                                                       default:
+                                                                               location.reload();
+                                                                               break;
+                                                               }
+                                                       })
                                        })
                                }, _('Save'))
                        ])
@@ -70,20 +81,20 @@ function handleAction(ev) {
 
        if (ev === 'qrcode') {
                return Promise.all([
-                       uci.load('wireless')
-               ]).then(function () {
-                       let w_sid, w_device, w_ssid, w_enc, w_key, w_hidden, result,
-                               w_sections = uci.sections('wireless', 'wifi-iface'),
-                               optionsAP = [E('option', { value: '' }, [_('-- AP Selection --')])];
-                       for (let i = 0; i < w_sections.length; i++) {
-                               if (w_sections[i].mode === 'ap' && w_sections[i].disabled !== '1') {
-                                       w_sid = i;
-                                       w_device = w_sections[i].device;
-                                       w_ssid = w_sections[i].ssid;
-                                       optionsAP.push(E('option', { value: w_sid }, w_device + ', ' + w_ssid));
+                       uci.load('wireless')])
+                       .then(function () {
+                               let w_sid, w_device, w_ssid, w_enc, w_key, w_hidden, result;
+                               const w_sections = uci.sections('wireless', 'wifi-iface');
+                               const optionsAP = [E('option', { value: '' }, [_('-- AP Selection --')])];
+                               for (let i = 0; i < w_sections.length; i++) {
+                                       if (w_sections[i].mode === 'ap' && w_sections[i].disabled !== '1') {
+                                               w_sid = i;
+                                               w_device = w_sections[i].device;
+                                               w_ssid = w_sections[i].ssid;
+                                               optionsAP.push(E('option', { value: w_sid }, w_device + ', ' + w_ssid));
+                                       }
                                }
-                       }
-                       let selectAP = E('select', {
+                               let selectAP = E('select', {
                                id: 'selectID',
                                class: 'cbi-input-select',
                                change: function (ev) {
@@ -100,16 +111,17 @@ function handleAction(ev) {
                                                } else {
                                                        w_enc = 'WPA';
                                                }
-                                               let data = `WIFI:S: ${w_ssid};T: ${w_enc};P: ${w_key};H: ${w_hidden};;`;
+                                               const data = `WIFI:S:${w_ssid};T:${w_enc};P:${w_key};H:${w_hidden};;`;
                                                const options = {
-                                                       pixelSize: 4,
+                                                       pixelSize: 12,
+                                                       margin: 1,
+                                                       ecLevel: 'M',
                                                        whiteColor: 'white',
                                                        blackColor: 'black'
                                                };
-                                               let svg = uqr.renderSVG(data, options);
+                                               const svg = uqr.renderSVG(data, options);
                                                result.innerHTML = svg.trim();
-                                       }
-                                       else {
+                                       } else {
                                                result.textContent = '';
                                        }
                                }
@@ -117,18 +129,15 @@ function handleAction(ev) {
                        L.ui.showModal(_('QR-Code Overview'), [
                                E('p', _('Render the QR-Code of the selected Access Point to transfer the WLAN credentials to your mobile devices comfortably.')),
                                E('div', { 'class': 'left', 'style': 'display:flex; flex-direction:column' }, [
-                                       E('label', { 'class': 'cbi-input-select', 'style': 'padding-top:.5em' }, [
-                                               selectAP,
-                                       ])
+                                       E('label', { 'class': 'cbi-input-select', 'style': 'padding-top:.5em' }, [selectAP,])
                                ]),
-                               '\xa0',
                                E('div', {
                                        'id': 'qrcode'
                                }),
                                E('div', { 'class': 'right' }, [
                                        E('button', {
                                                'class': 'cbi-button',
-                                               'click': L.hideModal
+                                       'click': L.hideModal
                                        }, _('Dismiss'))
                                ])
                        ]);
@@ -141,7 +150,7 @@ return view.extend({
                return Promise.all([
                        uci.load('travelmate'),
                        network.getWifiDevices().then(function (res) {
-                               let radios = [];
+                               const radios = [];
                                for (let i = 0; i < res.length; i++) {
                                        radios.push(res[i].sid);
                                }
@@ -160,18 +169,28 @@ return view.extend({
                        For further information <a href="https://github.com/openwrt/packages/blob/master/net/travelmate/files/README.md" target="_blank" rel="noreferrer noopener" >check the online documentation</a>. <br /> \
                        <b><em>Please note:</em></b> On first start please call the \'Interface Wizard\' once, to make the necessary network- and firewall settings.'));
 
+               /*
+                       set text content helper function
+               */
+               const setText = (id, value) => {
+                       const el = document.getElementById(id);
+                       if (el) {
+                               el.textContent = value || '-';
+                       }
+               };
+
                /*
                        poll runtime information
                */
                pollData: poll.add(function () {
                        return L.resolveDefault(fs.stat('/tmp/trm_runtime.json'), null).then(function (res) {
-                               let status = document.getElementById('status');
+                               const status = document.getElementById('status');
                                if (res && res.size > 0) {
                                        L.resolveDefault(fs.read_direct('/tmp/trm_runtime.json'), null).then(function (res) {
                                                if (res) {
                                                        let info = JSON.parse(res);
                                                        if (status && info) {
-                                                               status.textContent = (info.data.travelmate_status || '-') + ' / ' + (info.data.travelmate_version || '-');
+                                                               status.textContent = `${info.data.travelmate_status || '-'} (frontend: ${info.data.frontend_ver || '-'} / backend: ${info.data.backend_ver || '-'})`;
                                                                if (info.data.travelmate_status.startsWith('running')) {
                                                                        if (!status.classList.contains("spinning")) {
                                                                                status.classList.add("spinning");
@@ -187,33 +206,15 @@ return view.extend({
                                                                        status.classList.remove("spinning");
                                                                }
                                                        }
-                                                       let station_id = document.getElementById('station_id');
-                                                       if (station_id && info) {
-                                                               station_id.textContent = info.data.station_id || '-';
-                                                       }
-                                                       let station_mac = document.getElementById('station_mac');
-                                                       if (station_mac && info) {
-                                                               station_mac.textContent = info.data.station_mac || '-';
-                                                       }
-                                                       let station_interfaces = document.getElementById('station_interfaces');
-                                                       if (station_interfaces && info) {
-                                                               station_interfaces.textContent = info.data.station_interfaces || '-';
-                                                       }
-                                                       let station_subnet = document.getElementById('station_subnet');
-                                                       if (station_subnet && info) {
-                                                               station_subnet.textContent = info.data.station_subnet || '-';
-                                                       }
-                                                       let run_flags = document.getElementById('run_flags');
-                                                       if (run_flags && info) {
-                                                               run_flags.textContent = info.data.run_flags || '-';
-                                                       }
-                                                       let ext_hooks = document.getElementById('ext_hooks');
-                                                       if (ext_hooks && info) {
-                                                               ext_hooks.textContent = info.data.ext_hooks || '-';
-                                                       }
-                                                       let run = document.getElementById('run');
-                                                       if (run && info) {
-                                                               run.textContent = info.data.last_run || '-';
+                                                       if (info) {
+                                                               setText('station_id', info.data.station_id);
+                                                               setText('station_mac', info.data.station_mac);
+                                                               setText('station_interfaces', info.data.station_interfaces);
+                                                               setText('station_subnet', info.data.station_subnet);
+                                                               setText('run_flags', info.data.run_flags);
+                                                               setText('ext_hooks', info.data.ext_hooks);
+                                                               setText('run', info.data.last_run);
+                                                               setText('sys', info.data.system);
                                                        }
                                                }
                                        });
@@ -265,29 +266,9 @@ return view.extend({
                                        E('label', { 'class': 'cbi-value-title', 'style': 'margin-bottom:-5px;padding-top:0rem;' }, _('Last Run')),
                                        E('div', { 'class': 'cbi-value-field', 'id': 'run', 'style': 'margin-bottom:-5px;color:#37c;' }, '-')
                                ]),
-                               E('div', { class: 'right' }, [
-                                       E('button', {
-                                               'class': 'cbi-button cbi-button-apply',
-                                               'style': 'float:none;margin-right:.4em;',
-                                               'id': 'btn_suspend',
-                                               'click': ui.createHandlerFn(this, function () {
-                                                               return handleAction('qrcode');
-                                               })
-                                       }, [_('AP QR-Codes...')]),
-                                       E('button', {
-                                               'class': 'cbi-button cbi-button-negative',
-                                               'style': 'float:none;margin-right:.4em;',
-                                               'click': ui.createHandlerFn(this, function () {
-                                                       return handleAction('restart');
-                                               })
-                                       }, [_('Restart Interface')]),
-                                       E('button', {
-                                               'class': 'cbi-button cbi-button-negative',
-                                               'style': 'float:none;',
-                                               'click': ui.createHandlerFn(this, function () {
-                                                       return handleAction('setup');
-                                               })
-                                       }, [_('Interface Wizard...')])
+                               E('div', { 'class': 'cbi-value' }, [
+                                       E('label', { 'class': 'cbi-value-title', 'style': 'margin-bottom:-5px;padding-top:0rem;' }, _('System Info')),
+                                       E('div', { 'class': 'cbi-value-field', 'id': 'sys', 'style': 'margin-bottom:-5px;color:#37c;' }, '-')
                                ])
                        ]);
                }, o, this);
@@ -308,6 +289,12 @@ return view.extend({
                o = s.taboption('general', form.Flag, 'trm_enabled', _('Enabled'), _('Enable the travelmate service.'));
                o.rmempty = false;
 
+               o = s.taboption('general', widgets.NetworkSelect, 'trm_iface', _('WWAN Interface'), _('Select an existing wireless WAN network interface or create a new one with the \'Interface Wizard\'.'));
+               o.multiple = false;
+               o.nocreate = true;
+               o.optional = true;
+               o.rmempty = true;
+
                o = s.taboption('general', form.MultiValue, 'trm_radio', _('Radio Selection'), _('Restrict travelmate to certain radio\(s\).'));
                for (let i = 0; i < result[1].length; i++) {
                        o.value(result[1][i]);
@@ -315,7 +302,7 @@ return view.extend({
                o.placeholder = _('-- default --');
                o.optional = true;
                o.rmempty = true;
-               o.write = function(section_id, value) {
+               o.write = function (section_id, value) {
                        uci.set('travelmate', section_id, 'trm_radio', value.join(' '));
                };
 
@@ -456,10 +443,10 @@ return view.extend({
                */
                o = s.taboption('adv_email', form.DummyValue, '_sub');
                o.rawhtml = true;
-               o.default = '<em style="color:#37c;font-weight:bold;">' + _('E-Mail notifications require the separate setup of the <em>msmtp</em> package.') + '</em>'
-                       + '<hr style="width: 200px; height: 1px;" />'
+               o.default = '<em style="color:#37c;font-weight:bold;">' + _('Changes on this tab needs a travelmate service restart to take effect.') + '</em>'
+                       + '<hr style="width: 200px; height: 1px;" />';
 
-               o = s.taboption('adv_email', form.Flag, 'trm_mail', _('E-Mail Hook'), _('Sends notification E-Mails after every succesful uplink connect.'));
+               o = s.taboption('adv_email', form.Flag, 'trm_mail', _('E-Mail Notification'), _('Sends notification E-Mails after every succesful uplink connect.'));
                o.rmempty = false;
 
                o = s.taboption('adv_email', form.Value, 'trm_mailreceiver', _('E-Mail Receiver Address'), _('Receiver address for travelmate notification E-Mails.'));
@@ -482,7 +469,47 @@ return view.extend({
                o.placeholder = 'trm_notify';
                o.rmempty = true;
 
+               s = m.section(form.NamedSection, 'global');
+               s.render = L.bind(function () {
+                       return E('div', { 'class': 'cbi-page-actions' }, [
+                               E('button', {
+                                       'class': 'btn cbi-button cbi-button-negative important',
+                                       'style': 'float:none;margin-right:.4em;',
+                                       'title': 'Interface Setup',
+                                       'click': ui.createHandlerFn(this, function () {
+                                               return handleAction('setup');
+                                       })
+                               }, [_('Interface Wizard...')]),
+                               E('button', {
+                                       'class': 'btn cbi-button cbi-button-negative important',
+                                       'style': 'float:none;margin-right:.4em;',
+                                       'title': 'Restart Interface',
+                                       'click': ui.createHandlerFn(this, function () {
+                                               return handleAction('restartInterface');
+                                       })
+                               }, [_('Interface Restart')]),
+                               E('button', {
+                                       'class': 'btn cbi-button cbi-button-apply important',
+                                       'style': 'float:none;margin-right:.4em;',
+                                       'title': 'QRCode',
+                                       'id': 'btn_suspend',
+                                       'click': ui.createHandlerFn(this, function () {
+                                               return handleAction('qrcode');
+                                       })
+                               }, [_('AP QR-Codes...')]),
+                               E('button', {
+                                       'class': 'btn cbi-button cbi-button-positive important',
+                                       'style': 'float:none;margin-right:.4em;',
+                                       'title': 'Save & Restart',
+                                       'click': function () {
+                                               return handleAction('restartTravelmate');
+                                       }
+                               }, [_('Save & Restart')])
+                       ])
+               });
                return m.render();
        },
+       handleSaveApply: null,
+       handleSave: null,
        handleReset: null
 });
index 79a02994259f5c03742f69ca048f3264f85a2e95..660e7db9fced0a5f05201857a9fe4a57ffa345e7 100644 (file)
@@ -9,7 +9,7 @@
                "depends": {
                        "acl": [ "luci-app-travelmate" ],
                        "fs": {
-                               "/usr/bin/travelmate.sh": "executable",
+                               "/usr/bin/travelmate-service.sh": "executable",
                                "/etc/init.d/travelmate": "executable"
                        },
                        "uci": { "travelmate": true }
index 35fe60aeb990fa1ce15400a43282ddb807430b2a..2642282dfd0247aeb5aba5833199e6c1e25e797f 100644 (file)
                                "/var/run/travelmate.scan": [ "read" ],
                                "/var/state/travelmate.refresh": [ "read" ],
                                "/tmp/trm_runtime.json": [ "read" ],
-                               "/sbin/logread -e trm-": [ "exec" ],
-                               "/usr/sbin/logread -e trm-": [ "exec" ],
                                "/sbin/ifup *": [ "exec" ],
                                "/etc/init.d/travelmate start" : [ "exec" ],
+                               "/etc/init.d/travelmate restart" : [ "exec" ],
                                "/etc/init.d/travelmate stop" : [ "exec" ],
                                "/etc/init.d/travelmate setup [0-9a-z_]* [0-9a-z_]* [0-9]*" : [ "exec" ],
-                               "/etc/init.d/travelmate scan radio[0-1]" : [ "exec" ]
+                               "/etc/init.d/travelmate scan radio[0-9]" : [ "exec" ]
                        },
-                       "uci": [ "travelmate", "wireless" ]
+                       "uci": [ "travelmate", "wireless" ],
+                       "log": [ "read" ]
                }
        }
 }